/*
 * Ext GWT 2.2.5 - Ext for GWT
 * Copyright(c) 2007-2010, Ext JS, LLC.
 * licensing@extjs.com
 * 
 * http://extjs.com/license
 */
package com.extjs.gxt.ui.client.widget.menu;

import com.extjs.gxt.ui.client.GXT;
import com.extjs.gxt.ui.client.Style;
import com.extjs.gxt.ui.client.Style.HideMode;
import com.extjs.gxt.ui.client.aria.FocusFrame;
import com.extjs.gxt.ui.client.core.El;
import com.extjs.gxt.ui.client.core.XDOM;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.event.ClickRepeaterEvent;
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.ContainerEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.MenuEvent;
import com.extjs.gxt.ui.client.event.PreviewEvent;
import com.extjs.gxt.ui.client.util.BaseEventPreview;
import com.extjs.gxt.ui.client.util.ClickRepeater;
import com.extjs.gxt.ui.client.util.KeyNav;
import com.extjs.gxt.ui.client.util.Point;
import com.extjs.gxt.ui.client.util.Size;
import com.extjs.gxt.ui.client.widget.Component;
import com.extjs.gxt.ui.client.widget.Container;
import com.extjs.gxt.ui.client.widget.Layout;
import com.extjs.gxt.ui.client.widget.layout.MenuLayout;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Accessibility;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * A menu component.
 * 
 * <dl>
 * <dt><b>Events:</b></dt>
 * 
 * <dd><b>BeforeShow</b> : MenuEvent(container)<br>
 * <div>Fires before this menu is displayed. Listeners can cancel the action by
 * calling {@link BaseEvent#setCancelled(boolean)}.</div>
 * <ul>
 * <li>container : this</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>Show</b> : MenuEvent(container)<br>
 * <div>Fires after this menu is displayed.</div>
 * <ul>
 * <li>container : this</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>BeforeHide</b> : MenuEvent(container)<br>
 * <div>Fired before the menu is hidden. Listeners can cancel the action by
 * calling {@link BaseEvent#setCancelled(boolean)}.</div>
 * <ul>
 * <li>container : this</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>Hide</b> : MenuEvent(container)<br>
 * <div>Fires after this menu is hidden.</div>
 * <ul>
 * <li>container : this</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>BeforeAdd</b> : MenuEvent(container, item, index)<br>
 * <div>Fires before a item is added or inserted. Listeners can cancel the
 * action by calling {@link BaseEvent#setCancelled(boolean)}.</div>
 * <ul>
 * <li>container : this</li>
 * <li>item : the item being added</li>
 * <li>index : the index at which the item will be added</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>BeforeRemove</b> : MenuEvent(container, item)<br>
 * <div>Fires before a item is removed. Listeners can cancel the action by
 * calling {@link BaseEvent#setCancelled(boolean)}.</div>
 * <ul>
 * <li>container : this</li>
 * <li>item : the item being removed</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>Add</b> : MenuEvent(container, item, index)<br>
 * <div>Fires after a item has been added or inserted.</div>
 * <ul>
 * <li>container : this</li>
 * <li>item : the item that was added</li>
 * <li>index : the index at which the item will be added</li>
 * </ul>
 * </dd>
 * 
 * <dd><b>Remove</b> : MenuEvent(container, item)<br>
 * <div>Fires after a item has been removed.</div>
 * <ul>
 * <li>container : this</li>
 * <li>item : the item being removed</li>
 * </ul>
 * </dd>
 * </dl>
 * 
 * <dl>
 * <dt>Inherited Events:</dt>
 * <dd>BoxComponent Move</dd>
 * <dd>BoxComponent Resize</dd>
 * <dd>Component Enable</dd>
 * <dd>Component Disable</dd>
 * <dd>Component BeforeHide</dd>
 * <dd>Component Hide</dd>
 * <dd>Component BeforeShow</dd>
 * <dd>Component Show</dd>
 * <dd>Component Attach</dd>
 * <dd>Component Detach</dd>
 * <dd>Component BeforeRender</dd>
 * <dd>Component Render</dd>
 * <dd>Component BrowserEvent</dd>
 * <dd>Component BeforeStateRestore</dd>
 * <dd>Component StateRestore</dd>
 * <dd>Component BeforeStateSave</dd>
 * <dd>Component SaveState</dd>
 * </dl>
 */
public class Menu extends Container<Component> {

	protected KeyNav<ComponentEvent> keyNav;
	protected Item parentItem;
	protected BaseEventPreview eventPreview;
	protected boolean plain;
	protected boolean showSeparator = true;
	protected El ul;
	protected Item activeItem;

	private String subMenuAlign = "tl-tr?";
	private String defaultAlign = "tl-bl?";

	private int minWidth = 120;
	private boolean showing;
	private boolean constrainViewport = true;
	private boolean focusOnShow = true;
	private int maxHeight = Style.DEFAULT;
	private boolean enableScrolling = true;
	private int scrollIncrement = 24;
	private int scrollerHeight = 8;
	private int activeMax;

	/**
	 * Creates a new menu.
	 */
	public Menu() {
		baseStyle = "x-menu";
		shim = true;
		monitorWindowResize = true;
		setShadow(true);
		setLayoutOnChange(true);

		enableLayout = true;
		setLayout(new MenuLayout());
		eventPreview = new BaseEventPreview() {

			@Override
			protected boolean onPreview(PreviewEvent pe) {
				Menu.this.onAutoHide(pe);
				return super.onPreview(pe);
			}

			@Override
			protected void onPreviewKeyPress(PreviewEvent pe) {
				super.onPreviewKeyPress(pe);
				onEscape(pe);
			}
		};
		eventPreview.setAutoHide(false);
		// i18n stuff
		if (LocaleInfo.getCurrentLocale().isRTL()) {
			subMenuAlign = "tr-tl?";
			defaultAlign = "tr-bl?";
		}
	}

	/**
	 * Adds a item to the menu.
	 * 
	 * @param item the new item
	 */
	@Override
	public boolean add(Component item) {
		return super.add(item);
	}

	/**
	 * Returns the default alignment.
	 * 
	 * @return the default align
	 */
	public String getDefaultAlign() {
		return defaultAlign;
	}

	@Override
	public El getLayoutTarget() {
		return ul;
	}

	/**
	 * Returns the max height of the menu or -1 if not set.
	 * 
	 * @return the max height in pixels
	 */
	public int getMaxHeight() {
		return maxHeight;
	}

	/**
	 * Returns the menu's minimum width.
	 * 
	 * @return the width
	 */
	public int getMinWidth() {
		return minWidth;
	}

	/**
	 * Returns the menu's parent item.
	 * 
	 * @return the parent item
	 */
	public Item getParentItem() {
		return parentItem;
	}

	/**
	 * Returns the sub menu alignment.
	 * 
	 * @return the alignment
	 */
	public String getSubMenuAlign() {
		return subMenuAlign;
	}

	/**
	 * Hides the menu.
	 */
	@Override
	public void hide() {
		hide(false);
	}

	/**
	 * Hides this menu and optionally all parent menus
	 * 
	 * @param deep true to close all parent menus
	 * @return this
	 */
	public Menu hide(boolean deep) {
		if (showing) {
			MenuEvent me = new MenuEvent(this);
			if (fireEvent(Events.BeforeHide, me)) {
				if (activeItem != null) {
					activeItem.deactivate();
					activeItem = null;
				}
				onHide();
				RootPanel.get().remove(this);
				eventPreview.remove();
				showing = false;
				hidden = true;
				fireEvent(Events.Hide, me);
				if (deep && parentItem != null) {
					parentItem.parentMenu.hide(true);
				}
			}
		}
		return this;
	}

	/**
	 * Inserts an item into the menu.
	 * 
	 * @param item the item to insert
	 * @param index the insert location
	 */
	@Override
	public boolean insert(Component item, int index) {
		if (item instanceof Item) {
			((Item) item).parentMenu = this;
		}
		return super.insert(item, index);
	}

	/**
	 * Returns true if constrain to viewport is enabled.
	 * 
	 * @return the constrain to viewport state
	 */
	public boolean isConstrainViewport() {
		return constrainViewport;
	}

	/**
	 * Returns true if vertical scrolling is enabled.
	 * 
	 * @return true for scrolling
	 */
	public boolean isEnableScrolling() {
		return enableScrolling;
	}

	/**
	 * Returns true if the menu will be focused when displayed.
	 * 
	 * @return true if focused
	 */
	public boolean isFocusOnShow() {
		return focusOnShow;
	}

	@Override
	public boolean isVisible() {
		return showing;
	}

	@Override
	public void onComponentEvent(ComponentEvent ce) {
		super.onComponentEvent(ce);
		switch (ce.getEventTypeInt()) {
			case Event.ONCLICK:
				onClick(ce);
				break;
			case Event.ONMOUSEMOVE:
				onMouseMove(ce);
				break;
			case Event.ONMOUSEOUT:
				onMouseOut(ce);
				break;
			case Event.ONMOUSEOVER:
				onMouseOver(ce);
				break;
			case Event.ONMOUSEWHEEL:
				if (enableScrolling) {
					scrollMenu(ce.getEvent().getMouseWheelVelocityY() < 0);
				}
		}
		El t = ce.getTargetEl();
		if (enableScrolling && t.is(".x-menu-scroller")) {
			switch (ce.getEventTypeInt()) {
				case Event.ONMOUSEOVER:
					// deactiveActiveItem();
					onScrollerIn(t);
					break;
				case Event.ONMOUSEOUT:
					onScrollerOut(t);
					break;
			}
		}
	}

	/**
	 * Removes a item from the menu.
	 * 
	 * @param item the menu to remove
	 */
	@Override
	public boolean remove(Component item) {
		if (activeItem == item) {
			deactiveActiveItem();
		}
		boolean success = super.remove(item);
		if (success && item instanceof Item) {
			((Item) item).parentMenu = null;
		}
		return success;
	}

	/**
	 * Sets the active item. The component must be of type <code>Item</code> to be
	 * activated. All other types are ignored.
	 * 
	 * @param c the component to set active
	 * @param autoExpand true to auto expand the item
	 */
	public void setActiveItem(Component c, boolean autoExpand) {
		if (c == null) {
			deactiveActiveItem();
			return;
		}
		if (c instanceof Item) {
			Item item = (Item) c;
			if (item != activeItem) {
				deactiveActiveItem();

				this.activeItem = item;
				item.activate(autoExpand);
				item.el().scrollIntoView(ul.dom, false);
				focus();

				if (GXT.isFocusManagerEnabled()) {
					FocusFrame.get().frame(item);
					Accessibility.setState(getElement(), "aria-activedescendant", item.getId());
				}

			} else if (autoExpand) {
				item.expandMenu(autoExpand);
			}
		}
	}

	/**
	 * Sets whether the menu should be constrained to the viewport when shown.
	 * Only applies when using {@link #showAt(int, int)}.
	 * 
	 * @param constrainViewport true to constrain
	 */
	public void setConstrainViewport(boolean constrainViewport) {
		this.constrainViewport = constrainViewport;
	}

	/**
	 * Sets the default {@link El#alignTo} anchor position value for this menu
	 * relative to its element of origin (defaults to "tl-bl?").
	 * 
	 * @param defaultAlign the default align
	 */
	public void setDefaultAlign(String defaultAlign) {
		this.defaultAlign = defaultAlign;
	}

	/**
	 * True to enable vertical scrolling of the children in the menu (defaults to
	 * true).
	 * 
	 * @param enableScrolling true to for scrolling
	 */
	public void setEnableScrolling(boolean enableScrolling) {
		this.enableScrolling = enableScrolling;
	}

	/**
	 * True to set the focus on the menu when it is displayed.
	 * 
	 * @param focusOnShow true to focus
	 */
	public void setFocusOnShow(boolean focusOnShow) {
		this.focusOnShow = focusOnShow;
	}

	/**
	 * Sets the max height of the menu (defaults to -1). Only applies when
	 * {@link #setEnableScrolling(boolean)} is set to true.
	 * 
	 * @param maxHeight the max height
	 */
	public void setMaxHeight(int maxHeight) {
		this.maxHeight = maxHeight;
	}

	/**
	 * Sets he minimum width of the menu in pixels (defaults to 120).
	 * 
	 * @param minWidth the min width
	 */
	public void setMinWidth(int minWidth) {
		this.minWidth = minWidth;
	}

	/**
	 * The {@link El#alignTo} anchor position value to use for submenus of this
	 * menu (defaults to "tl-tr-?").
	 * 
	 * @param subMenuAlign the sub alignment
	 */
	public void setSubMenuAlign(String subMenuAlign) {
		this.subMenuAlign = subMenuAlign;
	}

	/**
	 * Displays this menu relative to another element.
	 * 
	 * @param elem the element to align to
	 * @param pos the {@link El#alignTo} anchor position to use in aligning to the
	 *          element (defaults to defaultAlign)
	 */
	public void show(Element elem, String pos) {
		show(elem, pos, new int[] {0, 0});
	}

	/**
	 * Displays this menu relative to another element.
	 * 
	 * @param elem the element to align to
	 * @param pos the {@link El#alignTo} anchor position to use in aligning to the
	 *          element (defaults to defaultAlign)
	 * @param offsets the menu align offsets
	 */
	public void show(Element elem, String pos, int[] offsets) {
		MenuEvent me = new MenuEvent(this);
		if (fireEvent(Events.BeforeShow, me)) {
			RootPanel.get().add(this);

			el().makePositionable(true);

			onShow();
			el().updateZIndex(0);

			showing = true;
			doAutoSize();

			el().alignTo(elem, pos, offsets);

			if (enableScrolling) {
				constrainScroll(el().getY());
			}
			el().show();

			eventPreview.add();

			if (focusOnShow) {
				focus();
			}

			fireEvent(Events.Show, me);
		}
	}

	/**
	 * Displays this menu relative to the widget using the default alignment.
	 * 
	 * @param widget the align widget
	 */
	public void show(Widget widget) {
		show(widget.getElement(), defaultAlign);
	}

	/**
	 * Displays this menu at a specific xy position.
	 * 
	 * @param x the x coordinate
	 * @param y the y coordinate
	 */
	public void showAt(int x, int y) {
		MenuEvent me = new MenuEvent(this);
		if (fireEvent(Events.BeforeShow, me)) {
			RootPanel.get().add(this);

			el().makePositionable(true);

			onShow();
			el().updateZIndex(0);

			if (LocaleInfo.getCurrentLocale().isRTL()) {
				Size size = getSize();
				x = x - size.width;
			}

			showing = true;
			doAutoSize();

			if (constrainViewport) {
				Point p = el().adjustForConstraints(new Point(x, y));
				x = p.x;
				y = p.y;
			}
			setPagePosition(x + XDOM.getBodyScrollLeft(), y + XDOM.getBodyScrollTop());
			if (enableScrolling) {
				constrainScroll(y);
			}

			el().show();
			eventPreview.add();

			if (focusOnShow) {
				focus();
			}

			fireEvent(Events.Show, me);
		}
	}
	@Override
	protected void afterRender() {
		super.afterRender();

		keyNav = new KeyNav<ComponentEvent>(this) {
			@Override
			public void onDown(ComponentEvent ce) {
				onKeyDown(ce);
			}

			@Override
			public void onEnter(ComponentEvent be) {
				if (activeItem != null) {
					be.cancelBubble();
					activeItem.onClick(be);
				}
			}

			@Override
			public void onLeft(ComponentEvent be) {
				hide();
				if (parentItem != null) {
					parentItem.parentMenu.focus();
					if (GXT.isFocusManagerEnabled()) {
						FocusFrame.get().frame(parentItem);
					}
				} else {
					Menu menu = Menu.this;
					while (menu.parentItem != null) {
						menu = menu.parentItem.parentMenu;
					}
					menu.fireEvent(Events.Minimize);
				}
			}

			@Override
			public void onRight(ComponentEvent be) {
				if (activeItem != null) {
					activeItem.expandMenu(true);
				}
				if (activeItem instanceof MenuItem) {
					MenuItem mi = (MenuItem) activeItem;
					if (mi.subMenu != null && mi.subMenu.isVisible()) {
						return;
					}
				}
				Menu menu = Menu.this;
				while (menu.parentItem != null) {
					menu = menu.parentItem.parentMenu;
				}
				menu.fireEvent(Events.Maximize);
			}

			@Override
			public void onUp(ComponentEvent ce) {
				onKeyUp(ce);
			}
		};
	}
	protected void constrainScroll(int y) {
		int full = ul.setHeight("auto").getHeight();

		int max = maxHeight != Style.DEFAULT ? maxHeight : (XDOM.getViewHeight(false) - y);
		if (full > max && max > 0) {
			activeMax = max - 10 - scrollerHeight * 2;
			ul.setHeight(activeMax, true);
			createScrollers();
		} else {
			ul.setHeight(full, true);
			NodeList<Element> nodes = el().select(".x-menu-scroller");
			for (int i = 0; i < nodes.getLength(); i++) {
				fly(nodes.getItem(i)).hide();
			}
		}
		ul.setScrollTop(0);
	}

	@Override
	protected ComponentEvent createComponentEvent(Event event) {
		return new MenuEvent(this);
	}

	@Override
	protected ContainerEvent<Menu, Component> createContainerEvent(Component item) {
		return new MenuEvent(this, item);
	}

	protected void createScrollers() {
		if (el().select(".x-menu-scroller").getLength() == 0) {
			Listener<ClickRepeaterEvent> listener = new Listener<ClickRepeaterEvent>() {
				@Override
				public void handleEvent(ClickRepeaterEvent be) {
					onScroll(be);
				}
			};

			El scroller;

			scroller = new El(DOM.createDiv());
			scroller.addStyleName("x-menu-scroller", "x-menu-scroller-top");
			scroller.setInnerHtml("&nbsp;");
			ClickRepeater cr = new ClickRepeater(scroller);
			cr.doAttach();
			cr.addListener(Events.OnClick, listener);
			addAttachable(cr);

			el().insertFirst(scroller.dom);

			scroller = new El(DOM.createDiv());
			scroller.addStyleName("x-menu-scroller", "x-menu-scroller-bottom");
			scroller.setInnerHtml("&nbsp;");
			cr = new ClickRepeater(scroller);
			cr.doAttach();
			cr.addListener(Events.OnClick, listener);
			addAttachable(cr);

			el().appendChild(scroller.dom);
		}
	}

	protected void deactiveActiveItem() {
		if (activeItem != null) {
			activeItem.deactivate();
			activeItem = null;
		}
		if (GXT.isFocusManagerEnabled()) {
			FocusFrame.get().unframe();
			Accessibility.setState(getElement(), "aria-activedescendant", "");
		}
	}
	/*
	 * This whole method is one big hack for IE's weird sizing issues when attaching a div to the root panel.
	 * We can't no the right width size so we guess, according to the inner text. If the figure we get does not make sense for
	 * a menu we try to stick with the original size IE calculated.
	 */
	private int getMaxTextWidth() {
		int maxWidth = getMinWidth();
		for (Component item : this.getItems()) {
			int textWidth = item.el().getTextWidth() + 36; // added place for images, checkbox or whatever + some margin
			if (maxWidth < textWidth)
				maxWidth = textWidth;
		}
		return maxWidth > 500 ? getLayoutTarget().getWidth() : maxWidth;
	}
	protected void doAutoSize() {
		if (showing && width == null) {
			int width;
			if (LocaleInfo.getCurrentLocale().isRTL() && GXT.isIE) {
				// ie calculates the width wrong on RTL so we use text width instead
				width = getMaxTextWidth()  + el().getFrameWidth("lr");
			}
			else {
				width = getLayoutTarget().getWidth() + el().getFrameWidth("lr");
			}
			el().setWidth(Math.max(width, minWidth), true);
		}
	}

	protected boolean onAutoHide(PreviewEvent pe) {
		if ((pe.getEventTypeInt() == Event.ONMOUSEDOWN || pe.getEventTypeInt() == Event.ONMOUSEWHEEL
				|| pe.getEventTypeInt() == Event.ONSCROLL || pe.getEventTypeInt() == Event.ONKEYPRESS)
				&& !(pe.within(getElement()) || (fly(pe.getTarget()).findParent(".x-ignore", -1) != null))) {
			MenuEvent me = new MenuEvent(this);
			me.setEvent(pe.getEvent());
			if (fireEvent(Events.AutoHide, me)) {
				hide(true);
				return true;
			}
		}
		return false;
	}

	protected void onClick(ComponentEvent ce) {
		Component item = findItem(ce.getTarget());
		if (item != null && item instanceof Item) {
			((Item) item).onClick(ce);
		}
	}

	@Override
	protected void onDetach() {
		super.onDetach();
		if (eventPreview != null) {
			eventPreview.remove();
		}
	}

	protected void onEscape(PreviewEvent pe) {
		if (pe.getKeyCode() == KeyCodes.KEY_ESCAPE) {
			if (activeItem != null && !activeItem.onEscape()) {
				return;
			}
			hide(false);
		}
	}

	@Override
	protected void onHide() {
		super.onHide();
		deactiveActiveItem();
	}

	@Override
	protected void onInsert(Component item, int index) {
		super.onInsert(item, index);
		if (rendered && GXT.isAriaEnabled() && item instanceof CheckMenuItem) {
			handleRadioGroups();
		}
	}

	protected void onKeyDown(ComponentEvent ce) {
		ce.stopEvent();
		if (tryActivate(indexOf(activeItem) + 1, 1) == null) {
			tryActivate(0, 1);
		}
	}

	protected void onKeyUp(ComponentEvent ce) {
		ce.stopEvent();
		if (tryActivate(indexOf(activeItem) - 1, -1) == null) {
			tryActivate(getItemCount() - 1, -1);
		}
	}

	@Override
	protected void onLayoutExcecuted(Layout layout) {
		super.onLayoutExcecuted(layout);
		doAutoSize();
	}
	protected void onMouseMove(ComponentEvent ce) {
	    Component c = findItem(ce.getTarget());
	    if (c != null && c instanceof Item) {
	      Item item = (Item) c;
	      if (activeItem != item && item.canActivate && item.isEnabled()) {
	        setActiveItem(item, true);
	      }
	    }
	  }
	
	protected void onMouseOut(ComponentEvent ce) {
		EventTarget to = ce.getEvent().getRelatedEventTarget();
		if (activeItem != null
				&& (to == null || (Element.is(to) && !DOM.isOrHasChild(activeItem.getElement(), (Element) Element.as(to))))
				&& activeItem.shouldDeactivate(ce)) {
			deactiveActiveItem();
		}
	}
	
	protected void onMouseOver(ComponentEvent ce) {
		EventTarget from = ce.getEvent().getRelatedEventTarget();
		if (from == null || (Element.is(from) && !DOM.isOrHasChild(getElement(), (Element) Element.as(from)))) {
			Component c = findItem(ce.getTarget());
			if (c != null && c instanceof Item) {
				Item item = (Item) c;
				if (activeItem != item && item.canActivate && item.isEnabled()) {
					setActiveItem(item, true);
				}
			}
		}
	}

	@Override
	protected void onRemove(Component item) {
		super.onRemove(item);
		if (rendered && GXT.isAriaEnabled() && item instanceof CheckMenuItem) {
			handleRadioGroups();
		}
	}

	@Override
	protected void onRender(Element target, int index) {
		setElement(DOM.createDiv(), target, index);
		el().makePositionable(true);
		super.onRender(target, index);

		ul = new El(DOM.createDiv());
		ul.addStyleName(baseStyle + "-list");

		getElement().appendChild(ul.dom);

		// add menu to ignore list
		eventPreview.getIgnoreList().add(getElement());

		el().setTabIndex(0);
		el().setElementAttribute("hideFocus", "true");

		el().addStyleName("x-ignore");
		if (GXT.isAriaEnabled()) {
			Accessibility.setRole(getElement(), "menu");
			Accessibility.setRole(ul.dom, "presentation");
			handleRadioGroups();
		}

		if (plain) {
			addStyleName("x-menu-plain");
		}
		if (!showSeparator) {
			addStyleName("x-menu-nosep");
		}

		sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.KEYEVENTS | Event.ONMOUSEWHEEL);
	}

	protected void onScroll(ClickRepeaterEvent ce) {
		El target = ce.getEl();
		boolean top = target.is(".x-menu-scroller-top");
		scrollMenu(top);

		if (top ? ul.getScrollTop() <= 0 : ul.getScrollTop() + activeMax >= ul.dom.getPropertyInt("scrollHeight")) {
			onScrollerOut(target);
		}
	}

	protected void onScrollerIn(El t) {
		boolean top = t.is(".x-menu-scroller-top");
		if (top ? ul.getScrollTop() > 0 : ul.getScrollTop() + activeMax < ul.dom.getPropertyInt("scrollHeight")) {
			t.addStyleName("x-menu-item-active", "x-menu-scroller-active");
		}
	}

	protected void onScrollerOut(El t) {
		t.removeStyleName("x-menu-item-active", "x-menu-scroller-active");
	}

	@Override
	protected void onWindowResize(int width, int height) {
		hide(true);
	}

	protected void scrollMenu(boolean top) {
		ul.setScrollTop(ul.getScrollTop() + scrollIncrement * (top ? -1 : 1));
	}

	protected Item tryActivate(int start, int step) {
		for (int i = start, len = getItemCount(); i >= 0 && i < len; i += step) {
			Component c = getItem(i);
			if (c instanceof Item) {
				Item item = (Item) c;
				if (item.canActivate && item.isEnabled()) {
					setActiveItem(item, false);
					return item;
				}
			}
		}
		return null;
	}

	private void clearGroups() {
		NodeList<Element> groups = el().select(".x-menu-radio-group");
		for (int i = 0; i < groups.getLength(); i++) {
			Element e = groups.getItem(i);
			El.fly(e).removeFromParent();
		}
	}

	private El getGroup(String groupName) {
		El g = el().selectNode("#" + getId() + "-" + groupName);
		if (g == null) {
			g = new El(DOM.createDiv());
			g.makePositionable(true);
			g.dom.setAttribute("role", "group");
			g.addStyleName(HideMode.OFFSETS.value());
			g.addStyleName("x-menu-radio-group");
			g.setId(getId() + "-" + groupName);
			el().appendChild(g.dom);
		}
		return g;
	}

	private void handleRadioGroups() {
		clearGroups();
		for (int i = 0; i < getItemCount(); i++) {
			Object obj = getItem(i);
			if (obj instanceof CheckMenuItem) {
				CheckMenuItem check = (CheckMenuItem) obj;
				if (check.getGroup() != null) {
					El g = getGroup(check.getGroup());
					Accessibility.setState(g.dom, "aria-owns", g.dom.getAttribute("aria-owns") + " " + check.getId());
					if (check.getAriaGroupTitle() != null) {
						g.dom.setTitle(check.getAriaGroupTitle());
					}
				}
			}
		}
	}
}
